Skip to content

<feature>[longjob]: Introduce suspend long job message#3241

Closed
ZStack-Robot wants to merge 1 commit into5.5.6from
sync/ye.zou/ZSTAC-81237@@2
Closed

<feature>[longjob]: Introduce suspend long job message#3241
ZStack-Robot wants to merge 1 commit into5.5.6from
sync/ye.zou/ZSTAC-81237@@2

Conversation

@ZStack-Robot
Copy link
Collaborator

APIImpact

Resolves: ZSTAC-81237

Change-Id: I6f7262716c7075637373646b726a796c73737563

sync from gitlab !9059

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Walkthrough

本变更为长期任务引入“暂停(suspend)”功能:新增 API 消息/事件、LongJob 接口与工厂的扩展、工厂实现的支持检测、管理器的暂停处理流程、可恢复错误判定、测试封装与中文文档,以及少量常量与钩子更新。

Changes

内聚组 / 文件(s) 变更摘要
API:消息与事件
header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsg.java, header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEvent.java
新增 APISuspendLongJobMsg(PUT /longjobs/{uuid}/actions)与 APISuspendLongJobEvent,包含注解、字段、getter/setter 与示例方法
接口签名
header/src/main/java/org/zstack/header/longjob/LongJob.java, longjob/src/main/java/org/zstack/longjob/LongJobFactory.java
在 LongJob 接口新增默认 suspend 方法;在 LongJobFactory 接口新增 supportSuspend(jobName) 签名
工厂实现
longjob/src/main/java/org/zstack/longjob/LongJobFactoryImpl.java
增加不支持 suspend 类型跟踪字段,新增 supportSuspend 实现,并在 checkBehaviorSupported 中检测 suspend 支持性
管理器实现
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java
路由 APISuspendLongJobMsg,新增 suspend 处理(ChainTask 排队、状态与支持性校验、调用 LongJob.suspend、状态更新、事件发布与错误处理)
错误处理工具
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java
getEventOnError 增加 isRecoverableError 判断(基于 opaque 字段 longJobRecoverable),将可恢复错误视为挂起而非失败
错误码常量
utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
新增三个静态常量:ORG_ZSTACK_AI_10131ORG_ZSTACK_AI_10132ORG_ZSTACK_AI_10133
测试辅助
testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
新增 suspendLongJob 测试封装方法(SuspendLongJobAction wrapper)
API 文档(中文)
header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy, header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy
新增中文 API 文档元数据,描述请求/响应字段与 REST 端点
测试配置
test/src/test/resources/zstack.properties
添加一个空行(在 identity.init.type= 之后)
Git 钩子
.gitconfig/hooks/commit-msg, .gitconfig/hooks/prepare-commit-msg
Shebang 从 python 改为 python3(解释器路径更新)

Sequence Diagram(s)

sequenceDiagram
    participant Client as "Client"
    participant API as "API Handler"
    participant Manager as "LongJobManager"
    participant Factory as "LongJobFactory"
    participant Job as "LongJob Impl"
    participant DB as "Database"

    Client->>API: APISuspendLongJobMsg(uuid)
    API->>Manager: handle(APISuspendLongJobMsg)
    Manager->>Manager: enqueue ChainTask("suspend-longjob-"+uuid)
    Manager->>DB: query LongJobVO(uuid)
    alt already Suspended
        Manager-->>API: publish APISuspendLongJobEvent(success)
    else state != Running
        Manager-->>API: publish APISuspendLongJobEvent(error)
    else
        Manager->>Factory: supportSuspend(jobName)?
        alt not supported
            Manager-->>API: publish APISuspendLongJobEvent(error)
        else supported
            Manager->>Job: suspend(LongJobVO, completion)
            alt suspend success
                Job->>DB: update state -> Suspended
                Manager-->>API: publish APISuspendLongJobEvent(success)
            else suspend failure
                Manager-->>API: publish APISuspendLongJobEvent(error)
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 我是代码里的小兔子,轻踩键盘去巡视,
暂停信号轻轻传,长工序安静入睡,
事件回响带着 UUID,错误可恢复我识别,
小兔一跳,变更就绪,悄悄把状态写回去。

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title follows the required format '[scope]: ' with 54 characters, well under the 72-character limit, and accurately describes the main change of introducing suspend long job message functionality.
Description check ✅ Passed The pull request description relates to the changeset by referencing the API impact of adding suspend long job functionality and resolving the specific issue ZSTAC-81237 that this change addresses.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.40.5)
utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java

Comment @coderabbitai help to get the list of available commands and usage tips.

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch 2 times, most recently from 6c098e1 to af47416 Compare January 25, 2026 03:56
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java`:
- Around line 484-501: The suspend path lacks the intermediate "Suspending"
state used by cancelLongJob, which can allow concurrent suspend calls to race;
before calling job.suspend(...) in suspendLongJob, set the intermediate state
(e.g., changeState(uuid, LongJobStateEvent.suspending) or the appropriate enum)
while holding the same synchronization (synchronized(jobUuids.intern(uuid))) as
cancelLongJob, then call job.suspend; on success move to Suspended (as now) and
on failure revert or record the proper state and propagate the error, mirroring
the cancelLongJob pattern (use the same locking, changeState calls, and error
handling around job.suspend to ensure consistency).
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (1)

463-472: 可考虑优化:减少重复数据库查询

在第 464-466 行查询了 statejobName,但在第 469 行又执行了一次 findByUuid 获取完整的 LongJobVO。可以考虑在需要返回 VO 时直接查询完整对象,或者延迟到真正需要时再查询。

♻️ 建议的优化
     private void suspendLongJob(String uuid, ReturnValueCompletion<LongJobVO> completion) {
         Tuple t = Q.New(LongJobVO.class).eq(LongJobVO_.uuid, uuid).select(LongJobVO_.state, LongJobVO_.jobName).findTuple();
         LongJobState currentState = t.get(0, LongJobState.class);
         String jobName = t.get(1, String.class);
 
         if (currentState == LongJobState.Suspended) {
-            LongJobVO vo = dbf.findByUuid(uuid, LongJobVO.class);
-            completion.success(vo);
+            completion.success(dbf.findByUuid(uuid, LongJobVO.class));
             return;
         }

Comment on lines +484 to +501
LongJobVO vo = dbf.findByUuid(uuid, LongJobVO.class);
LongJob job = longJobFactory.getLongJob(vo.getJobName());

job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) {
@Override
public void success(Boolean suspended) {
LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
completion.success(updatedVo);
}

@Override
public void fail(ErrorCode errorCode) {
logger.error(String.format("failed to suspend longjob [uuid:%s, name:%s]: %s", vo.getUuid(), vo.getName(), errorCode));
completion.fail(errorCode);
}
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# 查找 LongJobState 和 LongJobStateEvent 枚举定义
find . -name "*.java" -type f | xargs grep -l "enum LongJobState\|enum LongJobStateEvent" | head -5

Repository: MatheMatrix/zstack

Length of output: 253


🏁 Script executed:

# 在 LongJobManagerImpl.java 中查找 cancelLongJob 和其他状态转换方法
rg -n "cancelLongJob|suspendLongJob|resumeLongJob|changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 5 -B 1

Repository: MatheMatrix/zstack

Length of output: 6832


🏁 Script executed:

# 查找 LongJobState 和 LongJobStateEvent 的定义
rg -n "enum LongJobState|enum LongJobStateEvent" --type=java -A 25

Repository: MatheMatrix/zstack

Length of output: 4185


🏁 Script executed:

# 查看 Running 状态的所有可能转移
rg -n "Running\.transactions" header/src/main/java/org/zstack/header/longjob/LongJobState.java -A 10

Repository: MatheMatrix/zstack

Length of output: 750


🏁 Script executed:

# 查看 changeState 方法的实现
rg -n "private.*changeState|public.*changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 15

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 用不同方式查找 changeState 方法
rg -n "changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | head -20

Repository: MatheMatrix/zstack

Length of output: 824


🏁 Script executed:

# 查找 changeState 的完整实现
rg -n "def changeState|LongJobVO changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 20

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 使用 ast-grep 查找 changeState 方法
ast-grep --pattern 'LongJobVO changeState($_, $_) { $$$ }'

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 查找 changeState 方法定义,搜索返回类型
rg -n "changeState\(" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -B 2 | grep -E "private|public|protected" -A 2

Repository: MatheMatrix/zstack

Length of output: 1026


🏁 Script executed:

# 在整个文件中搜索方法签名
cat longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "changeState" -B 5 | head -50

Repository: MatheMatrix/zstack

Length of output: 2176


🏁 Script executed:

# 直接查看文件的特定行范围,寻找 changeState 方法定义
wc -l longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java

Repository: MatheMatrix/zstack

Length of output: 131


🏁 Script executed:

# 查找 changeState 的完整定义(在文件后部分)
tail -400 longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | head -200 | grep -n "changeState" -B 5 -A 15

Repository: MatheMatrix/zstack

Length of output: 4088


🏁 Script executed:

# 搜索所有的方法定义,找到 changeState
rg -n "^\s*(private|public|protected|static).*changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 用 sed 查找 changeState 方法定义
sed -n '900,1005p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "changeState" -B 5 -A 20

Repository: MatheMatrix/zstack

Length of output: 1126


🏁 Script executed:

# 使用 sed 直接查看文件特定行数范围
sed -n '750,850p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java

Repository: MatheMatrix/zstack

Length of output: 3523


🏁 Script executed:

# 搜索 LongJobVO 返回类型的方法定义
rg -n "LongJobVO.*changeState|changeState.*\{" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -B 1 -A 20 | head -80

Repository: MatheMatrix/zstack

Length of output: 4100


🏁 Script executed:

# 搜索 private/public 修饰的 changeState 方法
sed -n '800,900p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "changeState\|private\|public"

Repository: MatheMatrix/zstack

Length of output: 513


🏁 Script executed:

# 用 awk 找到 changeState 方法的确切位置
awk '/private.*changeState|public.*changeState/{print NR":"$0; for(i=1;i<=30;i++){getline; print NR":"$0}}' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | head -60

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 从后往前搜索 changeState 的方法定义
rg -n "^\s*private\s+LongJobVO\s+changeState|^\s*public\s+LongJobVO\s+changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 30

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 直接查看整个文件,搜索所有的 LongJobVO changeState 方法
grep -n "LongJobVO changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 25

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 搜索包含 changeState 的所有行
rg "changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -n

Repository: MatheMatrix/zstack

Length of output: 824


🏁 Script executed:

# 尝试搜索方法定义的其他模式
rg -n "changeState.*\(" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | tail -20

Repository: MatheMatrix/zstack

Length of output: 824


🏁 Script executed:

# 查看文件末尾部分,changeState 可能定义在那里
tail -200 longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java

Repository: MatheMatrix/zstack

Length of output: 7196


🏁 Script executed:

# 查找 LongJobManagerImpl 的类定义和继承
head -50 longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "class\|extends\|implements"

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 搜索所有文件中的 changeState 方法定义
rg -n "LongJobVO.*changeState\s*\(" --type=java -B 2 -A 20 | head -100

Repository: MatheMatrix/zstack

Length of output: 10796


可考虑添加中间状态以保持一致性,但当前设计可能依赖幂等性

对比 cancelLongJob 的实现,取消操作在调用 job.cancel() 之前会先设置 Canceling 中间状态(第 319 行),以防止并发请求干扰。而 suspendLongJob 在调用 job.suspend() 前没有设置中间状态,而是在成功后才通过 changeState() 改为 Suspended 状态(第 490 行)。

这意味着两个并发的 suspend 请求都可能通过 Running 状态检查(第 465 行)并调用 job.suspend()。不过值得注意的是:

  1. changeState() 方法使用了 synchronized (jobUuids.intern(uuid)) 进行同步化处理
  2. 状态机允许 Suspended → suspend → Suspended 的幂等转换

这表明当前设计可能依赖 suspend 操作的幂等性。建议明确文档说明 suspend 操作是否设计为幂等的,或考虑添加 Suspending 中间状态以与 cancel 的设计模式保持一致。

🤖 Prompt for AI Agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java` around
lines 484 - 501, The suspend path lacks the intermediate "Suspending" state used
by cancelLongJob, which can allow concurrent suspend calls to race; before
calling job.suspend(...) in suspendLongJob, set the intermediate state (e.g.,
changeState(uuid, LongJobStateEvent.suspending) or the appropriate enum) while
holding the same synchronization (synchronized(jobUuids.intern(uuid))) as
cancelLongJob, then call job.suspend; on success move to Suspended (as now) and
on failure revert or record the proper state and propagate the error, mirroring
the cancelLongJob pattern (use the same locking, changeState calls, and error
handling around job.suspend to ensure consistency).

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch 2 times, most recently from a5268ac to 0ac0d2c Compare January 25, 2026 06:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobFactory.java (1)

10-15: 新增接口方法建议补充 Javadoc
Line 14 新增接口方法但未补充 Javadoc,建议按规范补齐以便生成文档与保持一致性。

📝 建议修改
     boolean supportCancel(String jobName);
+    /**
+     * `@param` jobName long job name
+     * `@return` true if suspend is supported for the job type
+     */
     boolean supportSuspend(String jobName);

As per coding guidelines, ...

♻️ Duplicate comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (1)

484-501: cancelLongJob 相比缺少中间状态的问题已在之前的评审中提出。

当前实现在调用 job.suspend() 之前没有设置中间状态(如 Suspending),而 cancelLongJob 会在第 319 行先设置 Canceling 状态。虽然 changeState 内部使用了 synchronized(jobUuids.intern(uuid)) 进行同步,但这可能依赖于 suspend 操作的幂等性设计。

建议确认 suspend 操作是否设计为幂等的,或考虑添加 Suspending 中间状态以与 cancel 模式保持一致。

🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)

172-189: 建议将 "longJobRecoverable" 提取为常量
Line 185 使用字符串字面量作为跨模块约定的 key,存在拼写不一致风险。建议提为常量(必要时可公开)以减少魔法值。

♻️ 建议修改
 public class LongJobUtils {
     private static final CLogger logger = Utils.getLogger(LongJobUtils.class);
+    public static final String LONG_JOB_RECOVERABLE_OPAQUE_KEY = "longJobRecoverable";
@@
-        Object recoverable = errorCode.getFromOpaque("longJobRecoverable");
+        Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_OPAQUE_KEY);

As per coding guidelines, ...

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch from 0ac0d2c to 8d07640 Compare January 25, 2026 06:28
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)

160-169: 检查可恢复标记在聚合错误的递归传播路径

ErrorCodeList extends ErrorCode 并包含 List<ErrorCode> causes 字段,若业务侧将 longJobRecoverable 标记设置在 causes 列表中的错误对象的 opaque 字段上,当前的 isRecoverableError 方法(仅检查顶层 getFromOpaque)无法检测到该标记,导致判定为 fail 而非 suspend。建议补充递归遍历 causes 的逻辑,或在接口文档中明确要求该标记必须设置在顶层错误的 opaque 字段上。

🤖 Fix all issues with AI agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java`:
- Around line 487-493: The success handler for job.suspend currently ignores the
suspended Boolean and always transitions the job to Suspended; modify the
anonymous ReturnValueCompletion<Boolean> in the job.suspend(...) call so it
checks the suspended value and only calls changeState(uuid,
LongJobStateEvent.suspend) and completion.success(...) when suspended == true,
and otherwise call completion.fail(...) or completion.success(vo) (to keep
original state) per the method contract; update the success(Boolean suspended)
method in that block and ensure logging reflects whether the suspend actually
succeeded.
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)

182-186: 将 opaque key 抽成常量,避免魔法字符串
建议把 "longJobRecoverable" 提取为私有常量,降低拼写风险和复用成本。

♻️ 建议修改
 public class LongJobUtils {
     private static final CLogger logger = Utils.getLogger(LongJobUtils.class);
+    private static final String LONG_JOB_RECOVERABLE_OPAQUE_KEY = "longJobRecoverable";
@@
-        Object recoverable = errorCode.getFromOpaque("longJobRecoverable");
+        Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_OPAQUE_KEY);

Comment on lines +487 to +493
job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) {
@Override
public void success(Boolean suspended) {
LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
completion.success(updatedVo);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

必须处理 suspended 返回值,否则可能错误地标记为 Suspended。
当前无论 job.suspend(...) 返回 true/false 都会切到 Suspended。若实现返回 false 表示未真正暂停,这里会造成状态错误。

建议仅在 suspended == true 时变更状态,或按契约将 false 视为失败/保持原状态。

🔧 建议修正示例
job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) {
    `@Override`
    public void success(Boolean suspended) {
-        LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
-        logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
-        completion.success(updatedVo);
+        if (Boolean.TRUE.equals(suspended)) {
+            LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
+            logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
+            completion.success(updatedVo);
+        } else {
+            logger.info(String.format("longjob [uuid:%s, name:%s] suspend returned false; keep state unchanged", vo.getUuid(), vo.getName()));
+            completion.success(dbf.findByUuid(uuid, LongJobVO.class));
+        }
    }
🤖 Prompt for AI Agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java` around
lines 487 - 493, The success handler for job.suspend currently ignores the
suspended Boolean and always transitions the job to Suspended; modify the
anonymous ReturnValueCompletion<Boolean> in the job.suspend(...) call so it
checks the suspended value and only calls changeState(uuid,
LongJobStateEvent.suspend) and completion.success(...) when suspended == true,
and otherwise call completion.fail(...) or completion.success(vo) (to keep
original state) per the method contract; update the success(Boolean suspended)
method in that block and ensure logging reflects whether the suspend actually
succeeded.

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch 2 times, most recently from 50a007a to 06d193f Compare January 25, 2026 16:07
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy`:
- Around line 10-23: The documentation contains missing placeholder descriptions
for the APISuspendLongJobEvent fields; update the ref block for "inventory"
(path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory", clz
LongJobInventory) to include a concise Chinese description of what the
LongJobInventory represents (e.g., long job metadata being suspended), and
update the field block "success" to include a Chinese description indicating
whether the suspend operation succeeded (boolean true/false); ensure both desc
strings are meaningful, concise, and match existing doc style.
- Line 8: Replace the placeholder title string in
APISuspendLongJobEventDoc_zh_cn.groovy: change the line title "在这里输入结构的名称" to a
proper Chinese title such as "暂停长任务事件" (or the English identifier
"APISuspendLongJobEvent") so the document header accurately reflects the
structure; update only the title(...) call in that file to the chosen
descriptive text.
♻️ Duplicate comments (2)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (2)

487-493: 必须处理 suspended 返回值

当前实现忽略了 job.suspend() 的返回值 suspended,无论其为 true 还是 false 都会将状态变更为 Suspended。如果实现方返回 false 表示暂停未真正执行,这将导致状态与实际不一致。

建议仅在 suspended == true 时变更状态。

建议修复
 job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) {
     `@Override`
     public void success(Boolean suspended) {
-        LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
-        logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
-        completion.success(updatedVo);
+        if (Boolean.TRUE.equals(suspended)) {
+            LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
+            logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
+            completion.success(updatedVo);
+        } else {
+            logger.info(String.format("longjob [uuid:%s, name:%s] suspend returned false, state unchanged", vo.getUuid(), vo.getName()));
+            completion.success(dbf.findByUuid(uuid, LongJobVO.class));
+        }
     }

484-486: 建议:考虑添加中间状态以保持一致性

对比 cancelLongJob 在调用 job.cancel() 前会先设置 Canceling 中间状态(Line 319),而 suspendLongJob 在调用 job.suspend() 前没有设置中间状态。这可能导致并发请求同时通过 Running 状态检查。

如果 suspend 操作设计为幂等的,当前实现可以接受;否则建议添加 Suspending 中间状态。

🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)

182-190: 建议提取魔法值为常量并简化布尔判断逻辑。

根据编码规范,应避免使用魔法值。字符串 "longJobRecoverable" 建议定义为常量。同时,布尔判断可以使用 Boolean.TRUE.equals() 简化,更加简洁且能安全处理 null 和非 Boolean 类型。

♻️ 建议的重构
+    public static final String LONG_JOB_RECOVERABLE_KEY = "longJobRecoverable";
+
     /**
      * Check if an error is marked as recoverable in its opaque field.
      * Any business module can mark an error as recoverable by setting
      * "longJobRecoverable" to true in the error code's opaque field.
      * This allows the long job framework to automatically suspend instead of fail
      * for recoverable errors, enabling automatic retry after service restart.
      *
      * `@param` errorCode the error code to check
      * `@return` true if the error is marked as recoverable, false otherwise
      */
     private static boolean isRecoverableError(ErrorCode errorCode) {
-        // Check if error code has recoverable flag in opaque
-        // Any business module can set this flag to indicate the error is recoverable
-        Object recoverable = errorCode.getFromOpaque("longJobRecoverable");
-        if (recoverable instanceof Boolean && (Boolean) recoverable) {
-            return true;
-        }
-        return false;
+        Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_KEY);
+        return Boolean.TRUE.equals(recoverable);
     }


doc {

title "在这里输入结构的名称"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

文档标题未正确填写

title "在这里输入结构的名称" 是占位符文本,应当替换为实际的结构名称,例如 "暂停长任务事件""APISuspendLongJobEvent"

建议修复
-	title "在这里输入结构的名称"
+	title "暂停长任务事件"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
title "在这里输入结构的名称"
title "暂停长任务事件"
🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy`
at line 8, Replace the placeholder title string in
APISuspendLongJobEventDoc_zh_cn.groovy: change the line title "在这里输入结构的名称" to a
proper Chinese title such as "暂停长任务事件" (or the English identifier
"APISuspendLongJobEvent") so the document header accurately reflects the
structure; update only the title(...) call in that file to the chosen
descriptive text.

Comment on lines +10 to +23
ref {
name "inventory"
path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory"
desc "null"
type "LongJobInventory"
since "5.5.0"
clz LongJobInventory.class
}
field {
name "success"
desc ""
type "boolean"
since "5.5.0"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

字段描述缺失或为占位符

  • Line 13: desc "null" 应描述 inventory 字段的含义
  • Line 20: desc "" 应描述 success 字段的含义
建议修复
 	ref {
 		name "inventory"
 		path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory"
-		desc "null"
+		desc "长任务详情"
 		type "LongJobInventory"
 		since "5.5.0"
 		clz LongJobInventory.class
 	}
 	field {
 		name "success"
-		desc ""
+		desc "操作是否成功"
 		type "boolean"
 		since "5.5.0"
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ref {
name "inventory"
path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory"
desc "null"
type "LongJobInventory"
since "5.5.0"
clz LongJobInventory.class
}
field {
name "success"
desc ""
type "boolean"
since "5.5.0"
}
ref {
name "inventory"
path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory"
desc "Long job inventory details"
type "LongJobInventory"
since "5.5.0"
clz LongJobInventory.class
}
field {
name "success"
desc "Whether the operation succeeded"
type "boolean"
since "5.5.0"
}
🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy`
around lines 10 - 23, The documentation contains missing placeholder
descriptions for the APISuspendLongJobEvent fields; update the ref block for
"inventory" (path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory",
clz LongJobInventory) to include a concise Chinese description of what the
LongJobInventory represents (e.g., long job metadata being suspended), and
update the field block "success" to include a Chinese description indicating
whether the suspend operation succeeded (boolean true/false); ensure both desc
strings are meaningful, concise, and match existing doc style.

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch from 462a85c to 0ac6697 Compare January 26, 2026 10:24
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java`:
- Around line 259-276: In sendCallback, the Content-Length is computed using
responseBody.length() (characters) which is incorrect for non-ASCII text; change
it to compute the byte length using a fixed charset (e.g.,
StandardCharsets.UTF_8) and pass that long to headers.setContentLength; also
handle null responseBody by using 0 bytes. Locate sendCallback and replace the
current headers.setContentLength(...) call with a computation like responseBody
== null ? 0 : responseBody.getBytes(StandardCharsets.UTF_8).length and ensure
the rest of the method (HttpEntity construction) continues to use the
responseBody string as before.
- Around line 227-237: The small-body branch in TestLibController currently uses
reader.readLine() which strips line terminators and corrupts multi-line request
bodies; change the implementation for the contentLength < 1MB branch to read raw
characters (e.g., via reader.read(char[] buffer) in a loop) into the existing
StringBuilder (sb), checking sb.length() against maxSize after each read, and
return sb.toString(); ensure this matches the large-file code path behavior
(lines handling reader.read into a buffer) and keep the same variables:
contentLength, maxSize, reader, and StringBuilder sb.
- Around line 281-297: The sendErrorCallback method builds JSON by string
formatting which can produce invalid JSON if error.getMessage() contains special
characters; fix it by replacing the manual String.format("{\"error\":\"%s\"}",
...) with proper JSON serialization (e.g., use the project ObjectMapper or
Jackson) to serialize a small map or POJO like Collections.singletonMap("error",
error.getMessage()) via objectMapper.writeValueAsString(...), then use that
serialized string as errorBody (keep HttpHeaders/HttpEntity usage unchanged) so
the error message is correctly escaped and valid JSON.
- Line 495: The getOutputStream() override currently throws
UnsupportedOperationException which breaks handlers that write via
getOutputStream(); update TestLibController.getOutputStream() to return a
ServletOutputStream backed by a ByteArrayOutputStream (shared with getWriter()
so both writers write to the same response buffer) and provide accessor methods
to retrieve the buffered bytes; if you prefer a minimal change, at least change
the exception message to explain why getOutputStream() is unsupported and
reference getWriter() as the alternative. Ensure the returned
ServletOutputStream implements write(int)/isReady()/setWriteListener(...) to
behave like a real stream and that getWriter() wraps the same
ByteArrayOutputStream to keep responses consistent.
🧹 Nitpick comments (2)
testlib/src/main/java/org/zstack/testlib/TestLibController.java (2)

327-327: 未使用的字段 cachedReader

该字段已声明但从未被使用,可以移除。

♻️ 建议移除
     private static class CachedHttpServletRequest implements HttpServletRequest {
         private final HttpServletRequest delegate;
         private final String cachedBody;
-        private BufferedReader cachedReader;

517-523: 字符编码硬编码为 UTF-8,应使用请求的编码。

如果请求使用非 UTF-8 编码,这里的转换可能会导致内容损坏。此外,UTF-8 是 Java 保证支持的编码,UnsupportedEncodingException 永远不会抛出。

♻️ 建议改进
-        public CachedServletInputStream(String body) {
-            try {
-                this.bodyBytes = body != null ? body.getBytes("UTF-8") : new byte[0];
-            } catch (java.io.UnsupportedEncodingException e) {
-                throw new RuntimeException(e);
-            }
+        public CachedServletInputStream(String body) {
+            this.bodyBytes = body != null ? body.getBytes(java.nio.charset.StandardCharsets.UTF_8) : new byte[0];
         }

如需支持多种编码,可考虑将请求的 getCharacterEncoding() 传入此类。

Comment on lines 227 to 257
if (contentLength > 0 && contentLength < 1024 * 1024) {
// For small bodies (< 1MB), use StringBuilder with known capacity
StringBuilder sb = new StringBuilder(contentLength);
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
if (sb.length() > maxSize) {
throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
}
}
return sb.toString();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

readLine() 会丢失换行符,导致请求体内容损坏。

使用 reader.readLine() 读取内容时,换行符会被丢弃。对于包含换行符的 JSON 或多行文本内容,这会导致数据损坏。此外,这与 Line 239-251 的大文件处理路径行为不一致。

🐛 建议修复:统一使用字符缓冲区读取
-            if (contentLength > 0 && contentLength < 1024 * 1024) {
-                // For small bodies (< 1MB), use StringBuilder with known capacity
-                StringBuilder sb = new StringBuilder(contentLength);
-                String line;
-                while ((line = reader.readLine()) != null) {
-                    sb.append(line);
-                    if (sb.length() > maxSize) {
-                        throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
-                    }
-                }
-                return sb.toString();
-            } else {
-                // For larger or unknown size, read in chunks with size tracking
-                StringBuilder sb = new StringBuilder(8192);
-                char[] buffer = new char[8192];
-                int charsRead;
-                long totalRead = 0;
-                while ((charsRead = reader.read(buffer)) != -1) {
-                    totalRead += charsRead;
-                    if (totalRead > maxSize) {
-                        throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
-                    }
-                    sb.append(buffer, 0, charsRead);
-                }
-                return sb.toString();
+            // Use char buffer to preserve all characters including newlines
+            int initialCapacity = (contentLength > 0 && contentLength < 1024 * 1024) ? contentLength : 8192;
+            StringBuilder sb = new StringBuilder(initialCapacity);
+            char[] buffer = new char[8192];
+            int charsRead;
+            long totalRead = 0;
+            while ((charsRead = reader.read(buffer)) != -1) {
+                totalRead += charsRead;
+                if (totalRead > maxSize) {
+                    throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
+                }
+                sb.append(buffer, 0, charsRead);
             }
+            return sb.toString();
🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` around lines
227 - 237, The small-body branch in TestLibController currently uses
reader.readLine() which strips line terminators and corrupts multi-line request
bodies; change the implementation for the contentLength < 1MB branch to read raw
characters (e.g., via reader.read(char[] buffer) in a loop) into the existing
StringBuilder (sb), checking sb.length() against maxSize after each read, and
return sb.toString(); ensure this matches the large-file code path behavior
(lines handling reader.read into a buffer) and keep the same variables:
contentLength, maxSize, reader, and StringBuilder sb.

Comment on lines 259 to 296
private void sendCallback(String callbackUrl, String taskUuid, String responseBody, int statusCode) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(RESTConstant.TASK_UUID, taskUuid);
headers.setContentLength(responseBody != null ? responseBody.length() : 0);

HttpEntity<String> entity = new HttpEntity<>(responseBody != null ? responseBody : "", headers);
ResponseEntity<String> response = callbackRestTemplate.exchange(
callbackUrl, HttpMethod.POST, entity, String.class);

logger.debug(String.format("Sent callback for taskUuid[%s] to %s, status: %s",
taskUuid, callbackUrl, response.getStatusCode()));
} catch (Exception e) {
logger.error(String.format("Failed to send callback for taskUuid[%s] to %s",
taskUuid, callbackUrl), e);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Content-Length 使用字符数而非字节数,对非 ASCII 内容会不正确。

responseBody.length() 返回的是字符数,而 Content-Length 头应该是字节数。对于包含中文或其他多字节字符的响应体,这会导致长度不匹配。

🐛 建议修复
-            headers.setContentLength(responseBody != null ? responseBody.length() : 0);
-            
-            HttpEntity<String> entity = new HttpEntity<>(responseBody != null ? responseBody : "", headers);
+            String body = responseBody != null ? responseBody : "";
+            byte[] bodyBytes = body.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+            headers.setContentLength(bodyBytes.length);
+            
+            HttpEntity<String> entity = new HttpEntity<>(body, headers);
🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` around lines
259 - 276, In sendCallback, the Content-Length is computed using
responseBody.length() (characters) which is incorrect for non-ASCII text; change
it to compute the byte length using a fixed charset (e.g.,
StandardCharsets.UTF_8) and pass that long to headers.setContentLength; also
handle null responseBody by using 0 bytes. Locate sendCallback and replace the
current headers.setContentLength(...) call with a computation like responseBody
== null ? 0 : responseBody.getBytes(StandardCharsets.UTF_8).length and ensure
the rest of the method (HttpEntity construction) continues to use the
responseBody string as before.

Comment on lines 281 to 317
private void sendErrorCallback(String callbackUrl, String taskUuid, Throwable error) {
try {
String errorBody = String.format("{\"error\":\"%s\"}", error.getMessage());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(RESTConstant.TASK_UUID, taskUuid);
headers.setContentLength(errorBody.length());

HttpEntity<String> entity = new HttpEntity<>(errorBody, headers);
callbackRestTemplate.exchange(callbackUrl, HttpMethod.POST, entity, String.class);

logger.debug(String.format("Sent error callback for taskUuid[%s] to %s", taskUuid, callbackUrl));
} catch (Exception e) {
logger.error(String.format("Failed to send error callback for taskUuid[%s] to %s",
taskUuid, callbackUrl), e);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

错误消息未进行 JSON 转义,可能生成无效的 JSON。

如果 error.getMessage() 包含引号、反斜杠或换行符等特殊字符,生成的 JSON 将是无效的。建议使用 JSON 序列化库来构建响应。

🐛 建议修复:使用 JSON 工具类进行转义
-            String errorBody = String.format("{\"error\":\"%s\"}", error.getMessage());
+            String errorMsg = error.getMessage() != null ? error.getMessage() : "Unknown error";
+            // Escape special characters for JSON
+            errorMsg = errorMsg.replace("\\", "\\\\")
+                               .replace("\"", "\\\"")
+                               .replace("\n", "\\n")
+                               .replace("\r", "\\r")
+                               .replace("\t", "\\t");
+            String errorBody = String.format("{\"error\":\"%s\"}", errorMsg);

或者更推荐使用项目中已有的 JSON 库(如 Jackson)进行序列化。

🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` around lines
281 - 297, The sendErrorCallback method builds JSON by string formatting which
can produce invalid JSON if error.getMessage() contains special characters; fix
it by replacing the manual String.format("{\"error\":\"%s\"}", ...) with proper
JSON serialization (e.g., use the project ObjectMapper or Jackson) to serialize
a small map or POJO like Collections.singletonMap("error", error.getMessage())
via objectMapper.writeValueAsString(...), then use that serialized string as
errorBody (keep HttpHeaders/HttpEntity usage unchanged) so the error message is
correctly escaped and valid JSON.

@Override public void setContentLengthLong(long len) {}
@Override public String getCharacterEncoding() { return "UTF-8"; }
@Override public String getContentType() { return contentType; }
@Override public javax.servlet.ServletOutputStream getOutputStream() throws IOException { throw new UnsupportedOperationException("Use getWriter()"); }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

getOutputStream() 抛出异常可能导致某些处理器无法正常工作。

如果被测试的 HTTP 处理器使用 getOutputStream() 而非 getWriter() 写入响应,这会导致测试意外失败。建议实现一个基于 ByteArrayOutputStream 的版本,或者至少在异常消息中说明原因。

💡 可选改进:实现 OutputStream 支持
+        private final java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();
+        private javax.servlet.ServletOutputStream servletOutputStream;
+
         `@Override`
         public javax.servlet.ServletOutputStream getOutputStream() throws IOException {
-            throw new UnsupportedOperationException("Use getWriter()");
+            if (servletOutputStream == null) {
+                servletOutputStream = new javax.servlet.ServletOutputStream() {
+                    `@Override`
+                    public void write(int b) throws IOException {
+                        outputStream.write(b);
+                    }
+                    `@Override`
+                    public boolean isReady() { return true; }
+                    `@Override`
+                    public void setWriteListener(javax.servlet.WriteListener listener) {}
+                };
+            }
+            return servletOutputStream;
         }
+
+        public String getContentAsString() {
+            printWriter.flush();
+            String writerContent = writer.toString();
+            if (!writerContent.isEmpty()) {
+                return writerContent;
+            }
+            return outputStream.toString(java.nio.charset.StandardCharsets.UTF_8);
+        }
🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` at line 495,
The getOutputStream() override currently throws UnsupportedOperationException
which breaks handlers that write via getOutputStream(); update
TestLibController.getOutputStream() to return a ServletOutputStream backed by a
ByteArrayOutputStream (shared with getWriter() so both writers write to the same
response buffer) and provide accessor methods to retrieve the buffered bytes; if
you prefer a minimal change, at least change the exception message to explain
why getOutputStream() is unsupported and reference getWriter() as the
alternative. Ensure the returned ServletOutputStream implements
write(int)/isReady()/setWriteListener(...) to behave like a real stream and that
getWriter() wraps the same ByteArrayOutputStream to keep responses consistent.

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch from 0ac6697 to a0bd0d6 Compare January 26, 2026 10:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java`:
- Around line 83-106: 在 handleFullyAsync 中不要让后台线程通过 CachedHttpServletRequest 的
delegate 访问原始 HttpServletRequest;在发送 response 之前用 readRequestBodySafely
读取正文并快照所有后台处理需要的数据(例如 URI、头部 headers、查询参数/表单参数、方法等),然后构造一个独立的、纯数据的请求 DTO
或一个不再委托原始 request 的 CachedHttpServletRequest 替代品,并把该独立对象传给
Test.handleHttp(…)(MockHttpServletResponse 保留用于捕获输出);确保后台任务中的任何
getHeader()/getRequestURI()/getParameter() 调用都来自快照对象而非原始
delegate,以避免在响应提交后访问被回收/复用的 request。
♻️ Duplicate comments (4)
testlib/src/main/java/org/zstack/testlib/TestLibController.java (4)

224-236: readLine() 会丢失换行符,导致请求体损坏
小体积分支用 readLine() 会丢弃换行符,多行 JSON/文本会被破坏,且与大体积分支行为不一致。

✅ 建议修复(统一用字符缓冲区读取)
-            if (contentLength > 0 && contentLength < 1024 * 1024) {
-                // For small bodies (< 1MB), use StringBuilder with known capacity
-                StringBuilder sb = new StringBuilder(contentLength);
-                String line;
-                while ((line = reader.readLine()) != null) {
-                    sb.append(line);
-                    if (sb.length() > maxSize) {
-                        throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
-                    }
-                }
-                return sb.toString();
-            } else {
-                // For larger or unknown size, read in chunks with size tracking
-                StringBuilder sb = new StringBuilder(8192);
-                char[] buffer = new char[8192];
-                int charsRead;
-                long totalRead = 0;
-                while ((charsRead = reader.read(buffer)) != -1) {
-                    totalRead += charsRead;
-                    if (totalRead > maxSize) {
-                        throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
-                    }
-                    sb.append(buffer, 0, charsRead);
-                }
-                return sb.toString();
-            }
+            // Use char buffer to preserve all characters including newlines
+            int initialCapacity = (contentLength > 0 && contentLength < 1024 * 1024) ? contentLength : 8192;
+            StringBuilder sb = new StringBuilder(initialCapacity);
+            char[] buffer = new char[8192];
+            int charsRead;
+            long totalRead = 0;
+            while ((charsRead = reader.read(buffer)) != -1) {
+                totalRead += charsRead;
+                if (totalRead > maxSize) {
+                    throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
+                }
+                sb.append(buffer, 0, charsRead);
+            }
+            return sb.toString();

258-266: Content-Length 使用字符数会导致长度不准确
responseBody.length() 是字符数,非字节数;包含非 ASCII 时会错误。

✅ 建议修复(按 UTF‑8 字节长度)
-            headers.setContentLength(responseBody != null ? responseBody.length() : 0);
-            
-            HttpEntity<String> entity = new HttpEntity<>(responseBody != null ? responseBody : "", headers);
+            String body = responseBody != null ? responseBody : "";
+            byte[] bodyBytes = body.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+            headers.setContentLength(bodyBytes.length);
+            
+            HttpEntity<String> entity = new HttpEntity<>(body, headers);

280-288: 错误 JSON 需要转义,避免生成无效 JSON
String.format 直接拼接 error.getMessage() 时,遇到引号/反斜杠/换行会破坏 JSON。建议至少做转义(或改用 JSON 序列化工具)。

✅ 建议修复(最小转义 + 正确 Content-Length)
-            String errorBody = String.format("{\"error\":\"%s\"}", error.getMessage());
+            String errorMsg = error.getMessage() != null ? error.getMessage() : "Unknown error";
+            errorMsg = errorMsg.replace("\\", "\\\\")
+                               .replace("\"", "\\\"")
+                               .replace("\n", "\\n")
+                               .replace("\r", "\\r")
+                               .replace("\t", "\\t");
+            String errorBody = String.format("{\"error\":\"%s\"}", errorMsg);
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.APPLICATION_JSON);
             headers.set(RESTConstant.TASK_UUID, taskUuid);
-            headers.setContentLength(errorBody.length());
+            byte[] bodyBytes = errorBody.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+            headers.setContentLength(bodyBytes.length);

499-499: getOutputStream() 直接抛异常会使部分处理器测试失败
若被测处理器使用 getOutputStream() 写响应,将导致测试异常。建议提供基于 ByteArrayOutputStream 的实现或明确兼容方式。

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch 5 times, most recently from bc0977b to 3dcf84d Compare January 26, 2026 12:38
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java`:
- Around line 118-120: Replace the Chinese inline comments in
TestLibController.java with equivalent English comments that convey the same
intent: describe that after the simulator finishes processing an active HTTP
callback is sent, note that EnvSpec.replyHttpCall may have already sent a
callback but we proactively send one here to ensure reliability in case
EnvSpec.replyHttpCall did not execute. Update the comments near the Simulator
handling and the EnvSpec.replyHttpCall mention so they are fully in English
while preserving original meaning and context.
♻️ Duplicate comments (5)
testlib/src/main/java/org/zstack/testlib/TestLibController.java (5)

247-257: 小体积分支仍会丢失换行符

reader.readLine() 会丢弃换行符,导致多行 JSON/文本被破坏,且与大体积分支行为不一致。建议统一使用字符缓冲读取。


281-286: Content-Length 应使用字节长度

responseBody.length() 是字符数,非字节数。对非 ASCII 内容会导致 Content-Length 不准确。


301-309: 错误回调 JSON 未做转义

String.format("{\"error\":\"%s\"}", error.getMessage()) 未转义特殊字符,可能生成无效 JSON。建议使用 JSON 序列化。


378-403: 异步线程仍依赖原始 HttpServletRequest

后台线程通过 delegate 访问请求对象(如 getHeader()/getRequestURI()),而响应已提交后容器不保证请求对象仍可用,可能导致不稳定。建议在返回前快照所需字段并改用独立对象传递。


531-531: MockHttpServletResponse 不支持 getOutputStream

若被测处理器使用 getOutputStream(),这里会抛异常导致测试失败。建议提供基于缓冲的实现或更明确的降级策略。

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch 2 times, most recently from e2df4a9 to 35d4231 Compare January 26, 2026 13:10
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@header/src/main/java/org/zstack/header/longjob/LongJob.java`:
- Around line 13-14: Add Javadoc to the new LongJob.suspend(LongJobVO,
ReturnValueCompletion<Boolean>) method (and update resume(LongJobVO,
ReturnValueCompletion<APIEvent>) if missing) that states this default
implementation is a no-op, documents the semantics of the parameters (LongJobVO
job and the completion callback), and clearly describes the expected behavior
for calling completion (e.g., invoke completion.success(true/false) on success
or completion.fail(Throwable) on error). Reference the LongJob interface and the
exact method signatures so the doc clarifies default behavior and the
caller/implementer contract for invoking the provided ReturnValueCompletion.

In `@longjob/src/main/java/org/zstack/longjob/LongJobUtils.java`:
- Around line 165-190: Extract the magic string "longJobRecoverable" into a
static final constant (e.g., LONG_JOB_RECOVERABLE) in LongJobUtils and replace
its usages inside isRecoverableError(ErrorCode) with that constant; also harden
isRecoverableError by null-checking errorCode and the opaque value (ensure
errorCode != null, getFromOpaque(...) != null) before instanceof/boolean checks
and return false on nulls to avoid NPEs.
♻️ Duplicate comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (1)

487-493: 必须处理 suspended 返回值,否则会错误标记为 Suspended。
Line 489-492 直接切换到 Suspended,忽略了 suspended 的实际结果,可能导致状态与真实暂停结果不一致。

🔧 建议修正
         job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) {
             `@Override`
             public void success(Boolean suspended) {
-                LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
-                logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
-                completion.success(updatedVo);
+                if (Boolean.TRUE.equals(suspended)) {
+                    LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
+                    logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
+                    completion.success(updatedVo);
+                } else {
+                    logger.info(String.format("longjob [uuid:%s, name:%s] suspend returned false; keep state unchanged",
+                            vo.getUuid(), vo.getName()));
+                    completion.success(dbf.findByUuid(uuid, LongJobVO.class));
+                }
             }

Comment on lines +13 to 14
default void suspend(LongJobVO job, ReturnValueCompletion<Boolean> completion) {}
default void resume(LongJobVO job, ReturnValueCompletion<APIEvent> completion) {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

新增接口方法请补充 Javadoc 说明。

suspend(...) 是新接口方法,按规范需 Javadoc,建议说明默认 no-op 行为及 completion 的期望调用方式。根据编码规范。

📝 建议补充的注释示例
+    /**
+     * Suspend the long job if supported.
+     * Default implementation is no-op; implementations should invoke completion.
+     */
     default void suspend(LongJobVO job, ReturnValueCompletion<Boolean> completion) {}
🤖 Prompt for AI Agents
In `@header/src/main/java/org/zstack/header/longjob/LongJob.java` around lines 13
- 14, Add Javadoc to the new LongJob.suspend(LongJobVO,
ReturnValueCompletion<Boolean>) method (and update resume(LongJobVO,
ReturnValueCompletion<APIEvent>) if missing) that states this default
implementation is a no-op, documents the semantics of the parameters (LongJobVO
job and the completion callback), and clearly describes the expected behavior
for calling completion (e.g., invoke completion.success(true/false) on success
or completion.fail(Throwable) on error). Reference the LongJob interface and the
exact method signatures so the doc clarifies default behavior and the
caller/implementer contract for invoking the provided ReturnValueCompletion.

Comment on lines +165 to +190
} else if (isRecoverableError(errorCode)) {
return LongJobStateEvent.suspend;
} else {
return LongJobStateEvent.fail;
}
}

/**
* Check if an error is marked as recoverable in its opaque field.
* Any business module can mark an error as recoverable by setting
* "longJobRecoverable" to true in the error code's opaque field.
* This allows the long job framework to automatically suspend instead of fail
* for recoverable errors, enabling automatic retry after service restart.
*
* @param errorCode the error code to check
* @return true if the error is marked as recoverable, false otherwise
*/
private static boolean isRecoverableError(ErrorCode errorCode) {
// Check if error code has recoverable flag in opaque
// Any business module can set this flag to indicate the error is recoverable
Object recoverable = errorCode.getFromOpaque("longJobRecoverable");
if (recoverable instanceof Boolean && (Boolean) recoverable) {
return true;
}
return false;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. 查找 getEventOnError 方法定义和调用点
echo "=== Looking for getEventOnError definition and calls ==="
rg -nP --type=java 'getEventOnError' longjob/

# 2. 查找相关的 opaque key 常量是否已存在
echo -e "\n=== Checking for existing OPAQUE constants ==="
rg -nP --type=java 'longJobRecoverable|OPAQUE.*KEY' longjob/

# 3. 查找 ErrorCode 处理模式
echo -e "\n=== Checking ErrorCode null handling patterns ==="
rg -nP --type=java 'if.*errorCode\s*==\s*null|errorCode\.get' longjob/ | head -20

Repository: MatheMatrix/zstack

Length of output: 920


🏁 Script executed:

#!/bin/bash
# 查看 LongJobManagerImpl.java 第 721 行的上下文,了解 errorCode 是否可能为空
echo "=== Context around line 721 in LongJobManagerImpl.java ==="
sed -n '710,735p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | cat -n

# 也需要查看完整的 getEventOnError 方法定义
echo -e "\n=== Full getEventOnError method ==="
sed -n '160,192p' longjob/src/main/java/org/zstack/longjob/LongJobUtils.java | cat -n

Repository: MatheMatrix/zstack

Length of output: 3296


🏁 Script executed:

#!/bin/bash
# 查看 fail 回调方法是否来自接口定义,以及 ErrorCode 是否应该允许为空
echo "=== Search for fail callback interface ==="
rg -nP --type=java 'void\s+fail\s*\(\s*ErrorCode' longjob/ | head -5

# 查看 ZStack 中 ErrorCode 的设计模式
echo -e "\n=== Check ErrorCode null patterns in broader codebase ==="
fd --type f -name "*.java" header | xargs rg -l 'ErrorCode.*null|null.*ErrorCode' | head -5

# 检查这个 opaque key 是否在其他地方被使用或定义
echo -e "\n=== Search for similar opaque key patterns ==="
rg -nP --type=java 'getFromOpaque|setToOpaque' longjob/ | head -10

Repository: MatheMatrix/zstack

Length of output: 1646


🏁 Script executed:

#!/bin/bash
# 查看 ErrorCode 接口/类的定义,了解它是否应该允许为空
echo "=== Check ErrorCode definition ==="
fd --type f -name "ErrorCode.java" | xargs cat -n | head -50

# 查看更多 getFromOpaque 的使用模式,了解是否有其他地方定义了相关常量
echo -e "\n=== Look for opaque key constants in ErrorCode or related classes ==="
rg -nP --type=java 'OPAQUE|Opaque' header/ | grep -i key | head -10

# 检查是否有已有的常量定义模式
echo -e "\n=== Check for constant definitions for opaque keys ==="
rg -nP --type=java 'static\s+final\s+String.*=' header/ | grep -i opaque | head -10

Repository: MatheMatrix/zstack

Length of output: 933


🏁 Script executed:

#!/bin/bash
# 查看 ErrorCode.java 的完整定义
echo "=== Check ErrorCode definition ==="
find . -name "ErrorCode.java" -path "*/header/*" -exec cat {} \; | head -100

# 看看 opaque 相关的实现
echo -e "\n=== Check opaque implementation in ErrorCode ==="
find . -name "ErrorCode.java" -path "*/header/*" -exec grep -A5 "putToOpaque\|getFromOpaque" {} \;

Repository: MatheMatrix/zstack

Length of output: 3078


将 opaque key 字符串提取为常量

根据编码规范,应避免魔法值。建议将 "longJobRecoverable" 字符串定义为 static final 常量,便于跨模块复用并防止拼写错误。此外,建议在 isRecoverableError() 方法中增加防御性的 null 检查,提高代码鲁棒性。

建议修改
+    private static final String LONG_JOB_RECOVERABLE_OPAQUE_KEY = "longJobRecoverable";
+
     private static boolean isRecoverableError(ErrorCode errorCode) {
+        if (errorCode == null) {
+            return false;
+        }
-        Object recoverable = errorCode.getFromOpaque("longJobRecoverable");
-        if (recoverable instanceof Boolean && (Boolean) recoverable) {
-            return true;
-        }
-        return false;
+        Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_OPAQUE_KEY);
+        return Boolean.TRUE.equals(recoverable);
     }
🤖 Prompt for AI Agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobUtils.java` around lines 165
- 190, Extract the magic string "longJobRecoverable" into a static final
constant (e.g., LONG_JOB_RECOVERABLE) in LongJobUtils and replace its usages
inside isRecoverableError(ErrorCode) with that constant; also harden
isRecoverableError by null-checking errorCode and the opaque value (ensure
errorCode != null, getFromOpaque(...) != null) before instanceof/boolean checks
and return false on nulls to avoid NPEs.

@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch from 35d4231 to 0fe93a9 Compare January 27, 2026 05:33
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy`:
- Line 20: 在 APISuspendLongJobMsgDoc_zh_cn.groovy 中,当前的请求描述字段 desc """""" 为空;请在类
APISuspendLongJobMsgDoc_zh_cn 或与其对应的描述块中为 desc 添加简短明确的中文说明(例如说明此 API
用于挂起指定的长任务并返回操作结果),以便文档完整并能清晰表达请求用途;定位时查找文件中名为 APISuspendLongJobMsgDoc_zh_cn
的文档定义并更新其 desc 字符串。
- Line 10: Replace the placeholder API description string in
APISuspendLongJobMsgDoc_zh_cn.groovy (the desc """在这里填写API描述""") with a concise,
accurate Chinese description of the API’s behavior (for example:
"""暂停一个长时间运行的任务""" or another phrasing that accurately describes that the API
suspends a long-running job), ensuring the desc field clearly explains the
endpoint's purpose and expected effect.
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobFactory.java (1)

10-15: 为新增接口方法补充 Javadoc 说明
Line 14 新增的 supportSuspend 建议补充英文 Javadoc,明确语义与返回值,便于 API 文档与维护一致。As per coding guidelines, please add Javadoc for interface methods.

♻️ 建议修改
+    /**
+     * Whether the long job supports suspend operation.
+     */
     boolean supportSuspend(String jobName);


category "longjob"

desc """在这里填写API描述"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

API 描述为占位符,需要填写实际内容。

当前描述 """在这里填写API描述""" 是占位符文本。建议替换为实际的 API 功能描述,例如:"""暂停一个长时间运行的任务"""

🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy`
at line 10, Replace the placeholder API description string in
APISuspendLongJobMsgDoc_zh_cn.groovy (the desc """在这里填写API描述""") with a concise,
accurate Chinese description of the API’s behavior (for example:
"""暂停一个长时间运行的任务""" or another phrasing that accurately describes that the API
suspends a long-running job), ensuring the desc field clearly explains the
endpoint's purpose and expected effect.


clz APISuspendLongJobMsg.class

desc """"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

请求描述为空。

desc """""" 为空,建议添加对请求的简要描述。

🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy`
at line 20, 在 APISuspendLongJobMsgDoc_zh_cn.groovy 中,当前的请求描述字段 desc """"""
为空;请在类 APISuspendLongJobMsgDoc_zh_cn 或与其对应的描述块中为 desc 添加简短明确的中文说明(例如说明此 API
用于挂起指定的长任务并返回操作结果),以便文档完整并能清晰表达请求用途;定位时查找文件中名为 APISuspendLongJobMsgDoc_zh_cn
的文档定义并更新其 desc 字符串。

APIImpact

Resolves: ZSTAC-81237

Change-Id: I6f7262716c7075637373646b726a796c73737563
Signed-off-by: AlanJager <ye.zou@zstack.io>
@MatheMatrix MatheMatrix force-pushed the sync/ye.zou/ZSTAC-81237@@2 branch from 0fe93a9 to 7dac565 Compare January 27, 2026 08:31
@MatheMatrix MatheMatrix deleted the sync/ye.zou/ZSTAC-81237@@2 branch January 27, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants